#!/usr/bin/python
"""
ftransc is a script that bundles up utilities for audio conversion.
"""

import os
import sys
import time
import base64
import urllib
import optparse
import subprocess
import ConfigParser

try:
    import mutagen
    import mutagen.id3
    import mutagen.mp3
    import mutagen.mp4
    import mutagen.asf
    import mutagen.flac
    import mutagen.musepack
    import mutagen.oggvorbis
    NO_TAGS = False
except ImportError:
    NO_TAGS = True

from ftransc.utils import tagmap, m3u_extract, pls_extract, xspf_extract
from ftransc.utils.convert import (
                                    convert_to_wav,
                                    convert_to_wma,
                                    convert_to_mpc,
                                    convert_to_mp3,
                                    convert_to_m4a,
                                    convert_to_ogg,
                                    convert_to_flac,
                                  )

#_______ global vars _________
VERSION = '4.3.5'
LOGFILE = '/dev/null'
SILENT  = False

class MetaTag(object):
    """
    handles tag extraction and insertion into and/or from audio files
    """
    __tag_mapping = tagmap.tags.copy()
    
    if not NO_TAGS:
        __id3_mapping = {
            'artist'        : mutagen.id3.TPE1, 
            'album'         : mutagen.id3.TALB, 
            'title'         : mutagen.id3.TIT2, 
            'genre'         : mutagen.id3.TCON, 
            'year'          : mutagen.id3.TDRC, 
            'tracknumber'   : mutagen.id3.TRCK,
            'composer'      : mutagen.id3.TCOM,
            'lyrics'        : mutagen.id3.USLT,
        }
        __opener = {
            '.mp3'          : mutagen.mp3.Open,
            '.wma'          : mutagen.asf.Open, 
            '.m4a'          : mutagen.mp4.Open, 
            '.flac'         : mutagen.flac.Open,
            '.mpc'          : mutagen.musepack.Open,
            '.ogg'          : mutagen.oggvorbis.Open, 
        }
    else:
        __id3_mapping = {}
        __opener      = {}

    exts = (    
                '.ogg', 
                '.mp3', 
                '.flac', 
                '.mp4', 
                '.aac', 
                '.m4a',
                '.mpc', 
                '.wma', 
                '.wmv',
           )

    def __init__(self, input_file):
        self.input_file = input_file
        self.tags = {
                'title'         : None, 
                'artist'        : None, 
                'album'         : None, 
                'year'          : None, 
                'genre'         : None, 
                'tracknumber'   : None,
                'composer'      : None,
                'lyrics'        : None,
        }
        self.extract()
    
    def extract(self):
        """
        extracts metadata tags from the audio file
        """
        tags = mutagen.File(self.input_file)
        ext = os.path.splitext(self.input_file)[1].lower()
        if ext in self.exts:
            for tag, key in self.__tag_mapping[ext].items():
                if key in tags:
                    self.tags[tag] = tags[key][0]
                elif tag == 'lyrics' and key == 'USLT':
                    for id3tag in tags:
                        if id3tag.startswith(key):
                            self.tags[tag] = tags[id3tag].text
    
    def insert(self, output_file):
        """
        inserts tags tags into an audio file.
        """        
        ext = os.path.splitext(output_file)[1].lower()
        if ext not in self.__opener:
            return 1
        tags = self.__opener[ext](output_file)
        for tag, value in self.tags.items():
            if value is None or tag not in self.__tag_mapping[ext]:
                continue
            if tag == 'tracknumber' and \
                (isinstance(value, list) or isinstance(value, tuple)) and\
                len(value) == 2:
                value = '%d/%d' % (value[0], value[1])
            if ext == '.mp3':
                if tag == 'lyrics':
                    tags[self.__tag_mapping[ext][tag]] = \
                                    self.__id3_mapping[tag](encoding=3, 
                                                            lang='eng', 
                                                            desc='lyrics',
                                                            text=u'%s' % value)
                else:
                    tags[self.__tag_mapping[ext][tag]] = \
                                self.__id3_mapping[tag](encoding=3, 
                                                        text=[u'%s' % value])
            elif ext in self.exts and ext != '.mp3':
                if tag == 'tracknumber' and ext == '.m4a':
                    try:
                        trkn = [int(i) for i in str(value).split('/')]
                        tags[self.__tag_mapping[ext][tag]] = \
                                [(trkn[0], trkn[1])]
                    except IndexError:
                        tags[self.__tag_mapping[ext][tag]] = [(trkn[0], 0)]
                else:
                    tags[self.__tag_mapping[ext][tag]] = [u'%s' % value]
        tags.save()


class CoverTag(object):
    """
    Handles insertion or extraction of album cover art
    """

    __tag_mapping = {
                        '.mp3'  : 'APIC:',
                        '.m4a'  : 'covr',
                        '.wma'  : None,
                        '.ogg'  : 'metadata_block_picture',
                        '.flac' : 'metadata_block_picture',
                        '.mpc'  : None,
                    }

    def __init__(self, filename):
        self.coverart = {   
                            'mime'  : 'image/jpeg', 
                            'type'  : 3, 
                            'ext'   : None, 
                            'data'  : None,
                        }
        self.extract(filename)

    def extract(self, input_file):
        ext = os.path.splitext(input_file)[1]
        if ext not in self.__tag_mapping:
            return
        tag = self.__tag_mapping[ext]
        if tag is None:
            return
        
        tags = mutagen.File(input_file)
        if tag in tags:
            self.coverart['ext'] = ext
            if ext == '.mp3':
                apic = tags[tag]
                self.coverart['mime'] = apic.mime
                self.coverart['data'] = apic.data
            elif ext == '.m4a':
                self.coverart['data'] = tags[tag][0]
            elif ext in ('.ogg', '.flac'):
                encoded_image = tags[tag][0]
                image = mutagen.flac.Picture(base64.b64decode(encoded_image))
                self.coverart['data'] = image.data
                self.coverart['mime'] = image.mime
        elif ext == '.mp3':
            for key in tags:
                if key.startswith(tag):
                    apic = tags[key]
                    self.coverart['mime'] = apic.mime
                    self.coverart['data'] = apic.data

    def insert(self, output_file):
        ext = os.path.splitext(output_file)[1]
        if ext not in self.__tag_mapping:
            return
        tag = self.__tag_mapping[ext]
        if tag is None:
            return
        if self.coverart['data'] is None:
            return

        if ext == '.m4a':
            tags = mutagen.mp4.MP4(output_file)
            if self.coverart['ext'] == '.mp3':
                if self.coverart['mime'] == 'image/png':
                    mime = mutagen.mp4.MP4Cover.FORMAT_PNG
                else:
                    mime = mutagen.mp4.MP4Cover.FORMAT_JPEG
                
                coverart = mutagen.mp4.MP4Cover(self.coverart['data'], mime)
                tags['covr'] = [coverart]
                tags.save()
                return

        elif ext == '.mp3':
            audio = mutagen.mp3.MP3(output_file, ID3=mutagen.id3.ID3)
            if self.coverart['ext'] in ('.m4a', '.ogg', '.flac'):
                apic = mutagen.id3.APIC(
                                            desc     = u'',
                                            encoding = 3,
                                            data     = self.coverart['data'],
                                            type     = self.coverart['type'],
                                            mime     = self.coverart['mime']
                                       )
                audio.tags.add(apic)
                audio.save()
                return

def check_deps(check=False):
    """
    checks whether all dependencies for this script are installed or not.
    """
    deps = {
            'mutagen-inspect'   : [],
            'ffmpeg'            : [
                                    'mp3', 
                                    'ogg', 
                                    'wma', 
                                    'm4a', 
                                    'flac', 
                                    'wav', 
                                    'mpc',
                                  ],
            'lame'              : ['mp3'],
            'flac'              : ['flac'],
            'faac'              : ['m4a'],
            'oggenc'            : ['ogg'],
            'mppenc'            : ['mpc'],
            }
    for dep in deps:
        pkg = subprocess.Popen(["which", dep], 
                               stdout=subprocess.PIPE).communicate()[0].strip() 
        if check:
            print2(dep + '...' + rd + " not installed" + nc if not pkg \
                    else dep + '...' + gr + " installed" + nc)
        else:
            if not pkg:
                print2("%s_______ %s not installed _______%s" % (rd, dep, nc))
                for fmt in deps[dep]:
                    supported_formats.remove(fmt)
    if check:
        raise SystemExit(0)

def upgrade_version(current_version):
    """
    upgrades to the current available version
    """
    
    trunk_url = 'http://ftransc.googlecode.com/svn/trunk/'
    tmp_dir = '/tmp/tmp-ftransc_upgrade-tmp'
    ftransc_doc_dir = '/usr/share/doc/ftransc'
    if os.environ['USER'] != 'root':
        raise SystemExit('try using "sudo", you have to be "root" on this one.')
    target  = '%s/version' % trunk_url
    try:
        latest  = map(int, urllib.urlopen(target).read().strip().split('.'))
    except IOError:
        raise SystemExit('ftransc upgrade failed: \033[1;31moffline\033[0m')
    current = map(int, current_version.split('.'))
    latest_version = '.'.join(map(str, latest))

    if latest > current:
        cmd = ['svn', 'export', trunk_url, tmp_dir]
        with open('/dev/null', 'w') as devnull:
            subprocess.Popen(cmd, 
                             stdout=subprocess.PIPE, 
                             stderr=devnull).communicate()
            os.chdir(ftransc_doc_dir)
            cmd = ['make', 'uninstall']
            subprocess.Popen(cmd, 
                             stdout=subprocess.PIPE, 
                             stderr=devnull).communicate()
            cmd = ['make', 'install']
            os.chdir(tmp_dir)
            subprocess.Popen(cmd, 
                             stdout=subprocess.PIPE, 
                             stderr=devnull).communicate()
            os.chdir('..')
            cmd = ['rm', '-r', '-f', tmp_dir]
            subprocess.Popen(cmd, 
                             stdout=subprocess.PIPE, 
                             stderr=devnull).communicate()
        raise SystemExit('upgraded from version [%s] to version [%s]' % \
                     (current_version, latest_version))
    raise SystemExit('You are already on the latest version.')

def print2(msg, noreturn=False):
    if not SILENT:
        if noreturn:
            print msg,
        else:
            print msg

supported_formats = set(['mp3', 'wma', 'wav', 'ogg', 'flac', 'm4a', 'mpc'])
quality_presets = {
}
converters = {
        "mp3"   : convert_to_mp3, 
        "ogg"   : convert_to_ogg, 
        "m4a"   : convert_to_m4a, 
        "wma"   : convert_to_wma, 
        "wav"   : convert_to_wav, 
        "flac"  : convert_to_flac,
        "mpc"   : convert_to_mpc,
        }

if __name__ == "__main__":
    #______________________ colors __________________________
    rd = "\033[1;31m"
    gr = "\033[1;32m"
    yl = "\033[1;33m"
    bl = "\033[1;34m"
    pk = "\033[1;35m"
    nc = "\033[0m"

    #______________________ options _________________________
    parser = optparse.OptionParser(usage="%prog [options] [files]", 
                                   version=VERSION)
    parser.add_option('-f', '--format', type=str, default='mp3', 
            help='audio format to convert to')
    parser.add_option('-q', '--quality', type=str, default='normal', 
            help='audio quality preset')
    parser.add_option('-c', '--check', dest='check', action='store_true', 
            help='check dependencies')
    parser.add_option('-r', '--remove', dest='remove', action='store_true', 
            help='remove original file after converting successfully')
    parser.add_option('-d', '--decode', dest='decode', action='store_true', 
            help='decode file .wav format')
    parser.add_option('-w', '--over', dest='overwrite', action='store_true', 
            help='overwrite destination file if it exists already')
    parser.add_option('-u', '--unlock', dest='unlock', action='store_true', 
            help='unlock a locked file and convert')
    parser.add_option('-n', '--no-tags', dest='no_tags', action='store_true', 
            default=False, help='Disable metadata support')
    parser.add_option('--directory', dest="walk", type=str, 
            help='convert all files inside the given directory')
    parser.add_option('--upgrade', action='store_true', default=False,
            help='upgrade to the latest available version')
    parser.add_option('-s', '--silent', action='store_true', default=False,
            help='Be silent, nothing is printed out')
    parser.add_option('-l', '--log', dest='logfile', default=LOGFILE,
            help='Write log message to the specified file')
    parser.add_option('--debug', action='store_true', default=False,
            help='Debug mode. Print everything to the screen.')
    parser.add_option('--notify', action='store_true', default=False,
            help='Show encoding summary notification')
    parser.add_option('--presets', default='/etc/ftransc/presets.conf',
            help='Use presets from the specified presets configuration file')
    parser.add_option('--m3u', help='Convert files in the m3u playlist file')
    parser.add_option('--pls', help='Convert files in the pls playlist file')
    parser.add_option('--xspf', help='Convert files in the xspf playlist file')
    parser.add_option('-o', '--outdir', 
            help='Put converted file into specified folder')
    opt, files = parser.parse_args()
    
    if opt.upgrade:
        upgrade_version(VERSION)

    if NO_TAGS:
        no_tags = NO_TAGS
    else:
        no_tags = opt.no_tags
    
    if os.environ['USER'] == 'root':
        raise SystemExit('It is not safe to run ftransc as root.')
    
    #_________________ nautilus scripts ___________________
    if 'convert to ' in sys.argv[0]:
        opt.format = sys.argv[0].split()[-1]
        opt.notify = True
        opt.logfile = '/tmp/ftransc.log'
    
    if opt.debug:
        LOGFILE = '/dev/stdout'
    elif opt.logfile:
        LOGFILE = opt.logfile
    
    SILENT = opt.silent

    files = list(set(files)) #remove duplicates
    files.sort()
    home = os.getcwd()
    fmt = opt.format.lower()
    qual = opt.quality.lower()
    check_deps(check=opt.check)
    if fmt in ("mp4", "m4a", "aac"): 
        fmt = "m4a"
    if fmt in ("mpc", "musepack"):
        fmt = "mpc"
    if opt.decode: 
        fmt = "wav"
    if fmt not in supported_formats:
        raise SystemExit("%s%s%s is not a supported format" % (rd, fmt, nc))
    if not os.path.isfile(opt.presets):
        raise SystemExit('The presets file [%s] does not exist' % opt.presets)
    elif fmt != 'wav':
        presets = ConfigParser.ConfigParser()
        presets.readfp(open(opt.presets))
        if qual not in presets.options(fmt):
            print2("%s%s%s invalid quality preset, using %s%s%s." % \
                    (rd, qual, nc, gr, 'normal', nc))
            qual = 'normal'
        preset = presets.get(fmt, qual)
    else:
        preset = None

    if len(files) < 1 and not opt.walk and not opt.m3u and not opt.pls and not opt.xspf:
        raise SystemExit("ftransc: no input file")
    if opt.walk is not None:
        walker = os.walk(opt.walk)
        dest_dir, dummy, files = walker.next()
        pwd = os.getcwd()
        os.chdir(dest_dir)

    outdir = ''
    if opt.outdir is not None:
        outdir = os.path.abspath(os.path.expanduser(opt.outdir))
        if not os.path.isdir(outdir):
            try:
                os.mkdir(outdir)
            except OSError:
                outdir = ''

    if opt.m3u is not None:
        files = m3u_extract(opt.m3u)
    elif opt.pls is not None:
        files = pls_extract(opt.pls)
    elif opt.xspf is not None:
        files = xspf_extract(opt.xspf)

    old_dir = ''
    total = len(files)
    fails = 0
    times = []
    for c, in_tuple in enumerate(
                   [(os.path.dirname(x), os.path.basename(x)) for x in files]):
        new_dir, ifile = in_tuple
        tic = time.time()
        logfile = open(LOGFILE, 'a', 0)
        ofile = os.path.splitext(ifile)[0] + "." + fmt 
        if outdir:
            ofile = outdir + os.sep + ofile
            if not outdir.endswith(os.sep):
                outdir += os.sep
        if new_dir:
            if not os.path.isabs(new_dir):
                new_dir = home + os.path.sep + new_dir

            new_dir = os.path.realpath(new_dir)
            if new_dir != os.getcwd():
                os.chdir(new_dir)
                print2("\n___ Working Directory '%s' ___ " % new_dir)
        else:
            if os.getcwd() != home and not os.walk:
                os.chdir(home)
                print2("\n___ Working Directory '%s' ___ " % home)

        if ofile == ifile:
            print2("%s%d/%d%s | %s%s%s | input = output | %sskipped%s" % \
                    (pk, c + 1, total, nc, bl, ifile, nc, yl, nc))
            fails += 1
            continue
        if not os.path.exists(ifile):
            print2("%s%d/%d%s | %s%s%s | %sdoes not exist%s" % \
                    (pk, c + 1, total, nc, bl, ifile, nc, rd, nc))
            fails += 1
            continue
        if os.path.isfile(ofile) and not opt.overwrite:
            print2("%s%d/%d%s | %s%s%s | use '-w' to overwrite | %sskipped%s" % \
                    (pk, c + 1, total, nc, bl, ifile, nc, yl, nc))
            fails += 1
            continue
        if os.path.isdir(ifile) and opt.walk is None:
            print2("%s%d/%d%s | %s%s%s |  use '--directory' | %sskipped%s" % \
                    (pk, c + 1, total, nc, bl, ifile, nc, yl, nc))
            fails += 1
            continue
        #_____________ lockfile creation ________________
        swp_file = ".%s.swp" % ifile
        if os.path.isfile(swp_file) and not opt.unlock:
            print2("%s%d/%d%s | %s%s%s | use '-u' to unlock | %sskipped%s" % \
                    (pk, c + 1, total, nc, bl, ifile, nc, yl, nc))
            fails += 1
            continue
        elif not os.path.isfile(swp_file):
            try:
                with open(swp_file, 'w'): 
                    pass
            except IOError:
                raise SystemExit("%sNo permissions%s to write to this folder" %\
                        (rd, nc))
        #______________ extract metadata ________________
        try:
            if not no_tags:
                metadata = MetaTag(ifile)
                albumart = CoverTag(ifile)
        except IOError:
            print2("%s%d/%d%s | %s%s%s | %sUnreadable%s"  % \
                    (pk, c + 1, total, nc, bl, ifile, nc, rd, nc))
            dummy = os.remove(swp_file)
            fails += 1
            continue
        #___________ audio convert ______________
        ifilename, in_ext = os.path.splitext(ifile)
        if not SILENT:
            os.system('printf "%s%d/%d%s | to %s | %s%s%s ... "' % \
                    (pk, c + 1, total, nc, fmt.upper(), bl, ifile, nc))
        else:
            print2('%s%d/%d%s | to %s | %s%s%s ...' % \
                    (pk, c + 1, total, nc, fmt.upper(), bl, ifile, nc))
        if converters[fmt](ifilename, in_ext, logfile, outdir, preset=preset):
            print2("%sSuccess%s" % (gr, nc), noreturn=True)
            if opt.remove: 
                dummy = os.remove(ifile)
            if fmt.lower() == "flac":
                dummy = os.remove(ifilename + '.wav')
            dummy = os.remove(swp_file)
        else:
            print2("%sFail%s" % (rd, nc))
            dummy = os.remove(swp_file)
            del metadata
            fails += 1
            continue
        #___________ insert metadata to new audio file ___________
        try:
            if not no_tags:
                metadata.insert(ofile)
                albumart.insert(ofile)
                del metadata
                del albumart
        except Exception, err:
            if opt.debug:
                print2("%s%s%s" % (rd, err.message, nc))
        logfile.close()
        toc = time.time()
        times.append(toc - tic)
        print2("| %d sec" % (toc - tic))

    
    notify_send = subprocess.Popen(["which", 'notify-send'], 
                        stdout=subprocess.PIPE).communicate()[0].strip()
    if opt.notify and notify_send:
        subprocess.Popen(
                [
                'notify-send', 
                'ftransc - the Audio Converter', 
                'Converted %d files to %s format\n\t%d PASSED\n\t%d FAILED' % \
                            (total, fmt.upper(), total - fails, fails)
                ],
                stdout=subprocess.PIPE).communicate()

    if opt.notify and times:
        print '_' * 80
        print 'average: %d sec per song' % int(sum(times)/(1.0*len(times)))
        hr = int(sum(times)  / 3600)
        mn = int((sum(times) % 3600) / 60)
        sc = int(sum(times)  % 3600  % 60)
        print 'total: %d songs converted in %d hrs, %d min and %d sec' %\
            (total, hr, mn, sc)
        print '_' * 80

